package edu.dublbo.app.generator.mapper;

import edu.dublbo.app.generator.advice.exception.OptErrorException;
import edu.dublbo.app.generator.entity.TBasicModel;
import edu.dublbo.app.generator.entity.TBasicModelDetail;
import edu.dublbo.app.generator.result.OptStatus;
import edu.dublbo.app.generator.util.DemoUtils;
import edu.dublbo.app.generator.util.FileOperator;
import edu.dublbo.app.generator.util.StrUtils;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;


/**
 * 代码文件工具类
 *
 * @author DubLBo, 2020-09-08 20:35
 */
public class CodeFileGenV2 {
    private static final Logger logger = LoggerFactory.getLogger(CodeFileGenV2.class);

    // 模板名
    private static final String TEMPLATE_DIR = "src/main/resources/tpls/ftl/";
    private static final String FILE_NAME_TABLE = "table.ftl";
    private static final String FILE_NAME_MODEL = "model.ftl";
    private static final String FILE_NAME_MAPPER_INTER = "mapper_inter.ftl";
    private static final String FILE_NAME_MAPPER_XML = "mapper_xml.ftl";
    private static final String FILE_NAME_SERVICE = "service.ftl";
    private static final String FILE_NAME_CONTROLLER = "controller.ftl";
    private static final String FILE_NAME_APP_RUNNER = "app.java.ftl";
    private static final String FILE_NAME_APP_CONFIG = "application.yml.ftl";
    private static final String FILE_NAME_POM = "pom.xml.ftl";
    private static final String FILE_NAME_MYBATIS_CONFIG = "mybatis.xml.ftl";

    private static final String[] APP_CONFIG_TEMPLATE_PATH = {
            TEMPLATE_DIR + FILE_NAME_APP_RUNNER,
            TEMPLATE_DIR + FILE_NAME_APP_CONFIG,
            TEMPLATE_DIR + FILE_NAME_POM,
            TEMPLATE_DIR + FILE_NAME_MYBATIS_CONFIG
    };

    private static final String[] MODULE_TEMPLATE_PATH = {
            TEMPLATE_DIR + FILE_NAME_TABLE,
            TEMPLATE_DIR + FILE_NAME_MODEL,
            TEMPLATE_DIR + FILE_NAME_MAPPER_INTER,
            TEMPLATE_DIR + FILE_NAME_MAPPER_XML,
            TEMPLATE_DIR + FILE_NAME_SERVICE,
            TEMPLATE_DIR + FILE_NAME_CONTROLLER
    };

    private static final String LANG_PACKAGE_PREFIX = "java.lang";
    // 项目名
    private String projectName;
    // 项目描述
    private String projectDesc;
    // 包名：即组织名称.项目名称
    private String packageName;
    private String organizationName;

    private CodeFileGenV2() {
    }

    private static class CodeFileGenV2Instance {
        private static final CodeFileGenV2 INSTANCE = new CodeFileGenV2();
    }

    public static CodeFileGenV2 getInstance() {
        return CodeFileGenV2Instance.INSTANCE;
    }

    public void configProjectData(String projectName, String projectDesc, String packageName) {
        this.projectName = projectName;
        this.organizationName = packageName;
        this.packageName = StrUtils.isEmpty(packageName) ? projectName : packageName + "." + projectName;
        this.projectDesc = projectDesc == null ? "" : projectDesc;
    }

    /**
     * 生成代码，用于代码浏览
     * @param model 模型
     * @param details 模型明细列表
     * @return 结果 Map
     */
//    public static Map<String, Object> generateCode(TBasicModel model, List<TBasicModelDetail> details) {
//        if (model == null || details == null || details.size() == 0) {
//            throw new OptErrorException(OptStatus.FAIL.code, "模型数据丢失，请刷新后重试");
//        }
//
//        Map<String, Object> dataMap = initBaseData(model, details);
//
//        List<String> tableStructureDemo = generateDemo(TEMPLATE_DIR + FILE_NAME_TABLE, dataMap);
//        List<String> entityDemo = generateDemo(TEMPLATE_DIR + FILE_NAME_MODEL, dataMap);
//        List<String> mapperInterfaceDemo = generateDemo(TEMPLATE_DIR + FILE_NAME_MAPPER_INTER, dataMap);
//        List<String> mapperXmlDemo = generateDemo(TEMPLATE_DIR + FILE_NAME_MAPPER_XML, dataMap);
//        List<String> serviceDemo = generateDemo(TEMPLATE_DIR + FILE_NAME_SERVICE, dataMap);
//        List<String> controllerDemo = generateDemo(TEMPLATE_DIR + FILE_NAME_CONTROLLER, dataMap);
//
//        Map<String, Object> res = new HashMap<>();
//        res.put("tableStructureDemo", String.join("", tableStructureDemo));
//        res.put("entityDemo", String.join("", entityDemo));
//        res.put("mapperInterfaceDemo", String.join("", mapperInterfaceDemo));
//        res.put("mapperXmlDemo", String.join("", mapperXmlDemo));
//        res.put("serviceDemo", String.join("", serviceDemo));
//        res.put("controllerDemo", String.join("", controllerDemo));
//
//        return res;
//    }

    /**
     * 生成项目配置文件，启动类的代码并压缩
     * <pre>
     *     1. pom.xml
     *     2. application.yml
     *     3. mybatis.yml
     *     4. 项目启动类 SpringBootApplication
     * </pre>
     *
     * @param zos 压缩文件输出流
     */
    public void zipProjectConfigCode(ZipOutputStream zos) {
        Map<String, Object> dataMap = new HashMap<>(16);
        dataMap.put("orgName", organizationName);
        dataMap.put("packageName", packageName);
        dataMap.put("projectName", projectName);
        dataMap.put("projectDesc", projectDesc);
        for (String tplPath : APP_CONFIG_TEMPLATE_PATH) {
            String filename = getConfigFilename(tplPath);
            if (StrUtils.isEmpty(filename)) continue;
            execReadAndZip(tplPath, dataMap, filename, zos);
        }
    }

    /**
     * 生成单个模块的代码并压缩，包括：
     * <pre>
     *      1. table 脚本
     *      2. java 实体类
     *      3. mapper 层：mapper接口类 和 mapper xml文件
     *      4. service 类
     *      5. controller 类
     * </pre>
     *
     * @param moduleName   模块名称
     * @param model        模型信息
     * @param modelDetails 模型明细列表：属性 - 表字段
     * @param zos          压缩文件输出流
     */
    public void zipModuleCode(String moduleName,
                              TBasicModel model,
                              List<TBasicModelDetail> modelDetails,
                              ZipOutputStream zos) {
        // 检查数据
        if (model == null || modelDetails == null || modelDetails.size() == 0) {
            logger.error("模型数据丢失，moduleName={}, model={}, modelDetails={}", moduleName, model, modelDetails);
            throw new OptErrorException(OptStatus.FAIL.code, "模型数据丢失，请刷新后重试");
        }

        // 初始化数据
        String modelName = model.getModelName();
        String busiName = DemoUtils.genBusiBeanName(modelName);
        Map<String, Object> dataMap = configData(moduleName, busiName, model, modelDetails);

        for (String templatePath : MODULE_TEMPLATE_PATH) {
            // 获取文件名称
            String filename = getModuleFilename(templatePath, moduleName, busiName, modelName);
            if (StrUtils.isEmpty(filename)) {
                logger.warn("获取模板【{}】对应的生成文件的文件名错误", templatePath);
                continue;
            }
            execReadAndZip(templatePath, dataMap, filename, zos);
        }

    }

    private static String genRequestPath(String moduleName, String busiName) {
        StringBuilder sb = new StringBuilder();
        if (!busiName.toLowerCase().startsWith(moduleName))
            sb.append("/").append(moduleName);

        for (char c : busiName.toCharArray()) {
            if (c >= 'A' && c <= 'Z') {
                sb.append("/").append((char) (c + 32));
            } else {
                sb.append(c);
            }
        }
        return sb.length() == 0 ? "/" : sb.toString();
    }

    private Map<String, Object> configData(String moduleName,
                                           String busiName,
                                           TBasicModel model,
                                           List<TBasicModelDetail> modelDetails) {
        Set<String> set = new HashSet<>();
        for (TBasicModelDetail de : modelDetails) {
            // 设置字段类型的长度
            StringBuilder tmp = new StringBuilder();
            tmp.append(de.getColType());
            int colLen = de.getColumnLength() == null ? 0 : de.getColumnLength();
            int defaultLen = de.getDefaultColLen() == null ? 0 : de.getDefaultColLen();
            if (colLen > 0) {
                // 设置了字段长度
                tmp.append("(").append(colLen).append(")");
            } else if (defaultLen > 0) {
                // 默认长度不为空
                tmp.append("(").append(defaultLen).append(")");
            }
            de.setColTypeShow(tmp.toString());

            // 需要导入额外的包
            String p = de.getQualifiedProType();
            if (!p.startsWith(LANG_PACKAGE_PREFIX)) set.add(p);
        }

        Map<String, Object> dataMap = new HashMap<>();
        dataMap.put("requestPath", genRequestPath(moduleName, busiName));
        dataMap.put("importPackageSet", set);
        dataMap.put("packageName", packageName);
        dataMap.put("moduleName", moduleName);
        dataMap.put("busiName", busiName);
        dataMap.put("modelName", model.getModelName());
        dataMap.put("tableName", model.getTableName());
        dataMap.put("modelDesc", model.getRemark());
        dataMap.put("modelDetails", modelDetails);
        return dataMap;
    }

    /**
     * 获取文件名
     */
    private String getModuleFilename(String tempPath, String moduleName, String busiName, String modelName) {
        String path = String.format("%s/src/main/java/%s/%s",
                projectName, packageName.replace('.', '/'), moduleName);
        String tpf = "";
        switch (tempPath) {
            case TEMPLATE_DIR + FILE_NAME_TABLE:
                tpf = path + "/entity/" + modelName + "Mysql.sql";
                break;
            case TEMPLATE_DIR + FILE_NAME_MODEL:
                tpf = path + "/entity/" + modelName + ".java";
                break;
            case TEMPLATE_DIR + FILE_NAME_MAPPER_INTER:
                tpf = path + "/mapper/" + modelName + "Mapper.java";
                break;
            case TEMPLATE_DIR + FILE_NAME_MAPPER_XML:
                tpf = projectName + "/src/main/resources/mapper/mysql/" + moduleName + "/" + modelName + "Mapper.xml";
                break;
            case TEMPLATE_DIR + FILE_NAME_SERVICE:
                tpf = path + "/service/" + busiName + "Service.java";
                break;
            case TEMPLATE_DIR + FILE_NAME_CONTROLLER:
                tpf = path + "/controller/" + busiName + "Controller.java";
                break;
            default:
                break;
        }

        return tpf;
    }

    private String getConfigFilename(String tempPath) {
        if (tempPath.endsWith(FILE_NAME_POM)) {
            return projectName + "/pom.xml";
        }

        if (tempPath.endsWith(FILE_NAME_APP_CONFIG)) {
            return projectName + "/src/main/resources/application.yml";
        }

        if (tempPath.endsWith(FILE_NAME_MYBATIS_CONFIG)) {
            return projectName + "/src/main/resources/config/mybatis.xml";
        }

        if (tempPath.endsWith(FILE_NAME_APP_RUNNER)) {
            return String.format("%s/src/main/java/%s/%sApp.java",
                    projectName,
                    packageName.replace('.', '/'),
                    projectName.substring(0, 1).toUpperCase() + projectName.substring(1));
        }

        return "";
    }

    /**
     * 执行模板匹配并输出到压缩流
     *
     * @param templatePath 模板文件路径名
     * @param dataMap      数据项
     * @param outFilename  输出的压缩文件名
     * @param zos          压缩文件流
     */
    private void execReadAndZip(String templatePath,
                                Map<String, Object> dataMap,
                                String outFilename,
                                ZipOutputStream zos) {
        try (StringWriter sw = new StringWriter()) {
            // FreeMarker 配置
            Configuration conf = new Configuration(Configuration.VERSION_2_3_0);
            // 加载模板
            Template template = conf.getTemplate(templatePath);
            template.process(dataMap, sw);
            // 添加到 zip
            zos.putNextEntry(new ZipEntry(outFilename));
            IOUtils.write(sw.toString(), zos, StandardCharsets.UTF_8);
            zos.closeEntry();
        } catch (IOException e) {
            e.printStackTrace();
            throw new OptErrorException(OptStatus.FAIL.code, "流错误：" + e.getMessage());
        } catch (TemplateException e) {
            e.printStackTrace();
            throw new OptErrorException(OptStatus.FAIL.code, "模板文件读取错误");
        }
    }
}
